{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "7bfc6b55-92d5-42e7-ad65-2fbb9891aa55",
   "metadata": {},
   "source": [
    "# 使用 SQL 回答问题\n",
    "\n",
    " > 构建一个问题回答系统，执行 SQL 查询以告知其响应。\n",
    "\n",
    "## 构建一个基于 SQL 数据的问答系统（Build a Question/Answering System over SQL Data）\n",
    "\n",
    "### 内容概述\n",
    "\n",
    "让一个大语言模型（LLM）系统查询**结构化数据**，与处理非结构化文本数据有本质区别。\n",
    "\n",
    "在处理非结构化文本时，通常的做法是生成嵌入（embeddings），然后在向量数据库中进行语义搜索。  \n",
    "而在处理**结构化数据**（如关系型数据库中的表格数据）时，常见做法是让 LLM 生成并执行一种**领域特定语言（DSL）** 的查询，例如 **SQL**。\n",
    "\n",
    "在本指南中，我们将介绍如何在数据库中的表格数据上构建一个基本的问答系统（Q&A system）。我们会分别使用：\n",
    "\n",
    "- **Chains（链式调用）**\n",
    "- **Agents（智能体）**\n",
    "\n",
    "这两种方式都能让我们对数据库中的数据提出自然语言问题，并获得自然语言形式的答案。\n",
    "\n",
    "它们之间的主要区别在于：\n",
    "\n",
    "- 使用 **Chain** 时，我们通常只执行一次预定义的 SQL 查询；\n",
    "- 使用 **Agent** 时，它可以**循环多次查询数据库**，直到获取足够的信息来回答问题。\n",
    "\n",
    "### ⚠️ 安全提示 ⚠️\n",
    "\n",
    "构建基于 SQL 数据库的问答系统，通常需要**执行由模型生成的 SQL 查询语句**。这种做法存在**固有的安全风险**。\n",
    "\n",
    "为降低风险，请务必确保：\n",
    "\n",
    "> 你的数据库连接权限应**尽可能最小化**，仅授予你的 Chain（链）或 Agent（智能体）**实际需要的访问权限**。\n",
    "\n",
    "这样虽然不能完全消除模型驱动系统所带来的风险，但可以**显著提高安全性**。\n",
    "\n",
    "例如：\n",
    "\n",
    "- 不要使用具有写权限或管理权限的数据库账户；\n",
    "- 限制对敏感表或字段的访问；\n",
    "- 使用只读用户账号进行查询；\n",
    "- 对生成的 SQL 进行校验或过滤（如白名单机制）；\n",
    "\n",
    "### 架构（Architecture）\n",
    "\n",
    "从高层次来看，这类系统的执行步骤通常包括：\n",
    "\n",
    "1. **将问题转换为 SQL 查询**：  \n",
    "   由模型将用户的自然语言问题转化为对应的 SQL 查询语句。\n",
    "\n",
    "2. **执行 SQL 查询**：  \n",
    "   在数据库中执行生成的 SQL 语句，获取相关数据结果。\n",
    "\n",
    "3. **根据查询结果回答问题**：  \n",
    "   模型使用查询返回的数据，生成自然语言的回答，反馈给用户。\n",
    "\n",
    "---\n",
    "\n",
    "### 补充说明：\n",
    "\n",
    "请注意，这种处理方式同样适用于对 **CSV 文件中的结构化数据** 进行查询。  \n",
    "如果你希望了解如何构建基于 CSV 数据的问答系统，可以参考我们的指南：  \n",
    "👉 [如何构建基于 CSV 数据的问答系统（How-to guide on question-answering over CSV data）](https://python.langchain.com/docs/how_to/sql_csv/)\n",
    "\n",
    "\n",
    "![示例图片](../../assets/imgs/sql_usecase.png)  \n",
    "\n",
    "### 示例数据（Sample Data）\n",
    "\n",
    "下面的示例将使用一个 **SQLite 数据库连接**，并基于 **Chinook 数据库** 进行操作。  \n",
    "Chinook 是一个示例数据库，它模拟了一个数字音像商店（Digital Media Store）的业务数据。\n",
    "\n",
    "请按照以下安装步骤，在**与当前 Notebook 相同的目录下**创建 `Chinook.db` 文件。  \n",
    "你也可以通过命令行下载并构建该数据库：\n",
    "\n",
    "---\n",
    "\n",
    "### ✅ 安装步骤（Installation Steps）\n",
    "\n",
    "sqlite3 下载链接：https://sqlite.org/2025/sqlite-tools-win-x64-3500100.zip\n",
    "\n",
    "```bash\n",
    "# 下载 Chinook SQLite 数据库文件\n",
    "curl -s https://raw.githubusercontent.com/lerocha/chinook-database/master/ChinookDatabase/DataSources/Chinook_Sqlite.sql | sqlite3 Chinook.db\n",
    "\n",
    "```\n",
    "\n",
    "这样你就可以在 Python 代码中轻松连接和使用这个数据库了。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "df6f58e1-2d0f-4a72-97c7-bb9a41b43c76",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sqlite\n",
      "['Album', 'Artist', 'Customer', 'Employee', 'Genre', 'Invoice', 'InvoiceLine', 'MediaType', 'Playlist', 'PlaylistTrack', 'Track']\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "\"[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\""
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain_community.utilities import SQLDatabase\n",
    "\n",
    "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n",
    "print(db.dialect)\n",
    "print(db.get_usable_table_names())\n",
    "db.run(\"SELECT * FROM Artist LIMIT 10;\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4f47b985-5692-41b4-bef9-955afe1c2977",
   "metadata": {},
   "source": [
    "我们现在有了一个可以查询的 SQL 数据库。现在，让我们尝试将它连接到一个大语言模型（LLM）上。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2e2503a6-f076-4a92-a4d6-d87dedec08f8",
   "metadata": {},
   "source": [
    "### Chains（链式调用）\n",
    "\n",
    "**Chains（链）** 是由一系列可预测的步骤组合而成。在 LangGraph 中，我们可以通过简单的节点序列来表示一个 Chain。\n",
    "\n",
    "下面我们创建一个流程，它接收一个问题，并依次执行以下步骤：\n",
    "\n",
    "1. **将问题转换为 SQL 查询**；\n",
    "2. **执行该 SQL 查询**；\n",
    "3. **使用查询结果回答原始问题**。\n",
    "\n",
    "---\n",
    "\n",
    "### ⚠️ 局限性说明\n",
    "\n",
    "这种结构并不能处理所有场景，例如：\n",
    "\n",
    "- 它会对**任何用户输入**都尝试生成并执行 SQL 查询，即使输入是像 `\"hello\"` 这样的问候语；\n",
    "- 有些问题需要**多次数据库查询**才能回答完整（比如涉及多个表或多个逻辑步骤的问题）。\n",
    "\n",
    "这些问题我们将在后面的 **Agents（智能体）** 部分中解决。\n",
    "\n",
    "---\n",
    "\n",
    "### 应用状态（Application State）\n",
    "\n",
    "LangGraph 中的应用状态（State）决定了：\n",
    "\n",
    "- 哪些数据作为输入传入应用；\n",
    "- 步骤之间传递哪些信息；\n",
    "- 最终输出哪些内容。\n",
    "\n",
    "通常我们会使用 `TypedDict` 来定义状态结构，也可以使用 Pydantic 的 `BaseModel`。\n",
    "\n",
    "---\n",
    "\n",
    "### 本例中的状态定义\n",
    "\n",
    "对于当前这个基于 SQL 的问答系统，我们的状态可以包含以下字段：\n",
    "\n",
    "```python\n",
    "from typing import TypedDict\n",
    "\n",
    "class State(TypedDict):\n",
    "    question: str           # 用户提出的问题\n",
    "    sql_query: str          # 生成的 SQL 查询语句\n",
    "    result_of_query: str    # 查询返回的结果\n",
    "    answer: str             # 最终生成的回答\n",
    "```\n",
    "\n",
    "这样，我们就可以在每个节点中逐步填充这些字段，完成整个流程。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "43bea38c-d2e5-43f2-9b72-568dcba4a7aa",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing_extensions import TypedDict\n",
    "\n",
    "\n",
    "class State(TypedDict):\n",
    "    question: str\n",
    "    query: str\n",
    "    result: str\n",
    "    answer: str"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cf5572da-35bb-49d1-9e8e-ddc57cc4b3e7",
   "metadata": {},
   "source": [
    "现在我们只需要编写一些函数来操作这个状态（State），并逐步填充它的内容即可。\n",
    "\n",
    "---\n",
    "\n",
    "### 第一步：将问题转换为 SQL 查询\n",
    "\n",
    "我们需要的第一个功能是：**接收用户的自然语言问题，并将其转化为对应的 SQL 查询语句**。\n",
    "\n",
    "为了**稳定、可靠地获取纯 SQL 查询结果**（避免包含 Markdown 格式、解释性文字或其他多余内容），我们将使用 **LangChain 的结构化输出抽象（structured output abstraction）**。\n",
    "\n",
    "这意味着我们可以定义一个输出格式（例如只返回 SQL 查询字符串），并让模型严格按照该格式输出，从而提升准确性和可控性。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "6aa3b625-2456-4b2e-bd25-fb4c8344a7bd",
   "metadata": {},
   "outputs": [],
   "source": [
    "import getpass\n",
    "import os\n",
    "\n",
    "try:\n",
    "    # load environment variables from .env file (requires `python-dotenv`)\n",
    "    from dotenv import load_dotenv\n",
    "\n",
    "    _ = load_dotenv()\n",
    "except ImportError:\n",
    "    pass\n",
    "\n",
    "if not os.environ.get(\"DASHSCOPE_API_KEY\"):\n",
    "  os.environ[\"DASHSCOPE_API_KEY\"] = getpass.getpass(\"Enter API key for OpenAI: \")\n",
    "\n",
    "from langchain_community.chat_models.tongyi import ChatTongyi\n",
    "\n",
    "llm  = ChatTongyi(\n",
    "    streaming=True,\n",
    "    name=\"qwen-turbo\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "760822f6-9f39-49b9-9fa9-bd67ad19d320",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================\u001b[1m System Message \u001b[0m================================\n",
      "\n",
      "\n",
      "Given an input question, create a syntactically correct \u001b[33;1m\u001b[1;3m{dialect}\u001b[0m query to\n",
      "run to help find the answer. Unless the user specifies in his question a\n",
      "specific number of examples they wish to obtain, always limit your query to\n",
      "at most \u001b[33;1m\u001b[1;3m{top_k}\u001b[0m results. You can order the results by a relevant column to\n",
      "return the most interesting examples in the database.\n",
      "\n",
      "Never query for all the columns from a specific table, only ask for a the\n",
      "few relevant columns given the question.\n",
      "\n",
      "Pay attention to use only the column names that you can see in the schema\n",
      "description. Be careful to not query for columns that do not exist. Also,\n",
      "pay attention to which column is in which table.\n",
      "\n",
      "Only use the following tables:\n",
      "\u001b[33;1m\u001b[1;3m{table_info}\u001b[0m\n",
      "\n",
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "Question: \u001b[33;1m\u001b[1;3m{input}\u001b[0m\n"
     ]
    }
   ],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "\n",
    "system_message = \"\"\"\n",
    "Given an input question, create a syntactically correct {dialect} query to\n",
    "run to help find the answer. Unless the user specifies in his question a\n",
    "specific number of examples they wish to obtain, always limit your query to\n",
    "at most {top_k} results. You can order the results by a relevant column to\n",
    "return the most interesting examples in the database.\n",
    "\n",
    "Never query for all the columns from a specific table, only ask for a the\n",
    "few relevant columns given the question.\n",
    "\n",
    "Pay attention to use only the column names that you can see in the schema\n",
    "description. Be careful to not query for columns that do not exist. Also,\n",
    "pay attention to which column is in which table.\n",
    "\n",
    "Only use the following tables:\n",
    "{table_info}\n",
    "\"\"\"\n",
    "\n",
    "user_prompt = \"Question: {input}\"\n",
    "\n",
    "query_prompt_template = ChatPromptTemplate(\n",
    "    [(\"system\", system_message), (\"user\", user_prompt)]\n",
    ")\n",
    "\n",
    "for message in query_prompt_template.messages:\n",
    "    message.pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "25ffd64d-dac5-4382-a922-9e790a531a4c",
   "metadata": {},
   "source": [
    "这个提示词（prompt）中包含了一些我们需要填充的参数，例如 SQL 方言（dialect）和表结构（table schemas）。  \n",
    "LangChain 的 `SQLDatabase` 对象提供了一些方法来帮助我们获取这些信息。\n",
    "\n",
    "在我们的 **write_query** 步骤中，只需要将这些参数填入提示词模板中，然后让模型生成对应的 SQL 查询语句即可："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "d6331734-16c7-438c-9842-3bb3a65355dd",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing_extensions import Annotated\n",
    "\n",
    "\n",
    "class QueryOutput(TypedDict):\n",
    "    \"\"\"Generated SQL query.\"\"\"\n",
    "\n",
    "    query: Annotated[str, ..., \"Syntactically valid SQL query.\"]\n",
    "\n",
    "\n",
    "def write_query(state: State):\n",
    "    \"\"\"Generate SQL query to fetch information.\"\"\"\n",
    "    prompt = query_prompt_template.invoke(\n",
    "        {\n",
    "            \"dialect\": db.dialect,\n",
    "            \"top_k\": 10,\n",
    "            \"table_info\": db.get_table_info(),\n",
    "            \"input\": state[\"question\"],\n",
    "        }\n",
    "    )\n",
    "    structured_llm = llm.with_structured_output(QueryOutput)\n",
    "    result = structured_llm.invoke(prompt)\n",
    "    return {\"query\": result[\"query\"]}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "6ef0a770-efd3-401d-888e-8f09b2e3114e",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'query': 'SELECT COUNT(*) AS NumberOfEmployees FROM Employee'}"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "write_query({\"question\": \"How many Employees are there?\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a4eb771f-cc08-4a4d-b2e8-24118a690b27",
   "metadata": {},
   "source": [
    "### 执行查询（Execute Query）\n",
    "\n",
    "这是构建 SQL Chain（链）过程中**最危险的一步**。\n",
    "\n",
    "在执行前，请**务必谨慎评估是否允许对你的数据自动执行查询**。为了降低风险：\n",
    "\n",
    "- 应尽可能**最小化数据库连接权限**（例如使用只读账户）；\n",
    "- 可以考虑在执行查询前加入一个**人工审批步骤**；\n",
    "\n",
    "---\n",
    "\n",
    "### 实现方式\n",
    "\n",
    "为了执行 SQL 查询，我们将从 `langchain-community` 中加载一个工具（tool）。  \n",
    "我们创建的 `execute_query` 节点只需封装这个工具即可：\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "ee91ee0e-963e-48b2-be08-1f8e99334e5d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.tools.sql_database.tool import QuerySQLDatabaseTool\n",
    "\n",
    "\n",
    "def execute_query(state: State):\n",
    "    \"\"\"Execute SQL query.\"\"\"\n",
    "    execute_query_tool = QuerySQLDatabaseTool(db=db)\n",
    "    return {\"result\": execute_query_tool.invoke(state[\"query\"])}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "30b91add-2434-4f77-9ea6-02599cc9cd5a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'result': '[(8,)]'}"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "execute_query({\"query\": \"SELECT COUNT(EmployeeId) AS EmployeeCount FROM Employee;\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d28d933-9d0c-4298-af1b-83f9e41031c6",
   "metadata": {},
   "source": [
    "### 生成答案（Generate Answer）\n",
    "\n",
    "最后，我们的最后一个步骤是：**根据从数据库中检索到的信息，生成对原始问题的自然语言回答**。\n",
    "\n",
    "这一步通常由语言模型（LLM）完成，它会将结构化的查询结果（如表格数据或聚合数值）转化为易于理解的回答。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "c64ffe7d-bc5b-43be-a032-6c3508b2d99f",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_answer(state: State):\n",
    "    \"\"\"Answer question using retrieved information as context.\"\"\"\n",
    "    prompt = (\n",
    "        \"Given the following user question, corresponding SQL query, \"\n",
    "        \"and SQL result, answer the user question.\\n\\n\"\n",
    "        f'Question: {state[\"question\"]}\\n'\n",
    "        f'SQL Query: {state[\"query\"]}\\n'\n",
    "        f'SQL Result: {state[\"result\"]}'\n",
    "    )\n",
    "    response = llm.invoke(prompt)\n",
    "    return {\"answer\": response.content}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "668ff61d-bb19-4463-a8fc-6680b1757b48",
   "metadata": {},
   "source": [
    "### 使用 LangGraph 进行流程编排（Orchestrating with LangGraph）\n",
    "\n",
    "最后，我们将整个应用程序**编译成一个图对象（graph object）**。  \n",
    "在本例中，我们只是将前面定义的三个步骤按顺序连接起来，形成一个**线性的执行流程**。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "9c11cebe-492b-441d-846c-055be84901d8",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.graph import START, StateGraph\n",
    "\n",
    "graph_builder = StateGraph(State).add_sequence(\n",
    "    [write_query, execute_query, generate_answer]\n",
    ")\n",
    "graph_builder.add_edge(START, \"write_query\")\n",
    "graph = graph_builder.compile()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1b2a3527-acfa-4008-9ffd-ca8a925bb3c7",
   "metadata": {},
   "source": [
    "LangGraph 还内置了用于可视化你的应用程序控制流程的工具："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "6501c494-ae44-4200-a4fb-3485e920ec78",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import Image, display\n",
    "\n",
    "display(Image(graph.get_graph().draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8ddd65fb-d881-442b-8016-2441b5adef0b",
   "metadata": {},
   "source": [
    "让我们来测试我们的应用程序！请注意，我们可以流式传输各个步骤的结果："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "7ac10229-6b54-4190-ba2e-700289738c5d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'write_query': {'query': 'SELECT COUNT(*) AS NumberOfEmployees FROM Employee'}}\n",
      "{'execute_query': {'result': '[(8,)]'}}\n",
      "{'generate_answer': {'answer': 'There are 8 employees.'}}\n"
     ]
    }
   ],
   "source": [
    "for step in graph.stream(\n",
    "    {\"question\": \"How many employees are there?\"}, stream_mode=\"updates\"\n",
    "):\n",
    "    print(step)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d6dcd732-535b-42ea-a5b4-efb82047230e",
   "metadata": {},
   "source": [
    "### 人机协同循环（Human-in-the-loop）  \n",
    "LangGraph 支持多项对此工作流有用的特性。其中之一便是“人机协同循环”：我们可以在某些敏感步骤（例如执行 SQL 查询）之前中断应用程序，以供人工审核。  \n",
    "这一功能由 LangGraph 的持久化层实现，它可以将运行进度保存到你选择的存储中。  \n",
    "以下示例中，我们指定使用内存中的存储："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "1c5dbadc-0a06-4f5e-bd76-f1e4107270e9",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "\n",
    "memory = MemorySaver()\n",
    "graph = graph_builder.compile(checkpointer=memory, interrupt_before=[\"execute_query\"])\n",
    "\n",
    "# Now that we're using persistence, we need to specify a thread ID\n",
    "# so that we can continue the run after review.\n",
    "config = {\"configurable\": {\"thread_id\": \"1\"}}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "7b6436dd-a758-4fd7-9e2a-163c9929acf6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(Image(graph.get_graph().draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "31e4fbe7-74e8-42b3-a3cf-f9d93849b1ac",
   "metadata": {},
   "source": [
    "让我们重复相同的运行过程，并添加一个简单的“是/否”审批步骤：\n",
    "```\n",
    "INSERT INTO [Track] ([TrackId], [Name], [AlbumId], [MediaTypeId], [GenreId], [Composer], [Milliseconds], [Bytes], [UnitPrice]) VALUES\n",
    "    (1, 'For Those About To Rock (We Salute You)', 1, 1, 1, 'Angus Young, Malcolm Young, Brian Johnson', 343719, 11170334, 0.99),\n",
    "    (2, 'Balls to the Wall', 2, 2, 1, 'U. Dirkschneider, W. Hoffmann, H. Frank, P. Baltes, S. Kaufmann, G. Hoffmann', 342562, 5510424, 0.99),\n",
    "\n",
    "INSERT INTO [Genre] ([GenreId], [Name]) VALUES\n",
    "    (1, 'Rock'),\n",
    "    (2, 'Jazz'),\n",
    "    (3, 'Metal'),\n",
    "    (4, 'Alternative & Punk'),\n",
    "    (5, 'Rock And Roll'),\n",
    "    (6, 'Blues'),\n",
    "    (7, 'Latin'),\n",
    "    (8, 'Reggae'),\n",
    "    (9, 'Pop'),\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "871efeee-b7c6-4452-9fd4-521bcd93ad80",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'write_query': {'query': 'SELECT COUNT(*) AS RockSongsCount FROM Track WHERE GenreId = 1'}}\n",
      "{'__interrupt__': ()}\n"
     ]
    },
    {
     "name": "stdin",
     "output_type": "stream",
     "text": [
      "您想去执行查询吗？（是/否）：  y\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'execute_query': {'result': '[(1297,)]'}}\n",
      "{'generate_answer': {'answer': '有 **1297** 首摇滚曲目。'}}\n"
     ]
    }
   ],
   "source": [
    "for step in graph.stream(\n",
    "    {\"question\": \"有多少首摇滚曲目?\"},\n",
    "    config,\n",
    "    stream_mode=\"updates\",\n",
    "):\n",
    "    print(step)\n",
    "\n",
    "try:\n",
    "    user_approval = input(\"您想去执行查询吗？（是/否）： \")\n",
    "except Exception:\n",
    "    user_approval = \"n\"\n",
    "\n",
    "if user_approval.lower() == \"y\":\n",
    "    # If approved, continue the graph execution\n",
    "    for step in graph.stream(None, config, stream_mode=\"updates\"):\n",
    "        print(step)\n",
    "else:\n",
    "    print(\"操作被用户取消。\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "124ab346-34ad-43f1-be3c-f34bf64eb47c",
   "metadata": {},
   "source": [
    "### 智能代理（Agents）\n",
    "\n",
    "智能代理利用大语言模型（LLMs）的推理能力，在执行过程中进行决策。使用代理可以将查询生成和执行过程中的更多判断权交给模型。虽然它们的行为相比前述“链式”方法（chain）更不可预测，但它们也具有一些优势：\n",
    "\n",
    "- 它们可以根据需要多次查询数据库，以回答用户的问题。  \n",
    "- 它们可以通过运行生成的查询、捕获错误回溯并重新生成正确查询来从错误中恢复。  \n",
    "- 它们可以根据数据库的结构（schema）以及数据库中的内容来回答问题（例如描述某个特定表的结构）。  \n",
    "\n",
    "下面我们构建一个最简单的 SQL 代理。我们将使用 LangChain 的 `SQLDatabaseToolkit` 为其配备一组工具。借助 LangGraph 预构建的 ReAct 代理构造器，我们可以在一行代码中完成这一操作。\n",
    "\n",
    "SQLDatabaseToolkit 包含以下功能的工具：\n",
    "\n",
    "- 创建并执行查询  \n",
    "- 检查查询语法  \n",
    "- 获取表的描述  \n",
    "- 以及其他更多功能"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "593576f3-3c20-4770-b3d2-ff0566dac2a4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[QuerySQLDatabaseTool(description=\"Input to this tool is a detailed and correct SQL query, output is a result from the database. If the query is not correct, an error message will be returned. If an error is returned, rewrite the query, check the query, and try again. If you encounter an issue with Unknown column 'xxxx' in 'field list', use sql_db_schema to query the correct table fields.\", db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x000001CE24DADDD0>),\n",
       " InfoSQLDatabaseTool(description='Input to this tool is a comma-separated list of tables, output is the schema and sample rows for those tables. Be sure that the tables actually exist by calling sql_db_list_tables first! Example Input: table1, table2, table3', db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x000001CE24DADDD0>),\n",
       " ListSQLDatabaseTool(db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x000001CE24DADDD0>),\n",
       " QuerySQLCheckerTool(description='Use this tool to double check if your query is correct before executing it. Always use this tool before executing a query with sql_db_query!', db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x000001CE24DADDD0>, llm=ChatTongyi(name='qwen-turbo', client=<class 'dashscope.aigc.generation.Generation'>, model_kwargs={}, dashscope_api_key=SecretStr('**********'), streaming=True), llm_chain=LLMChain(verbose=False, prompt=PromptTemplate(input_variables=['dialect', 'query'], input_types={}, partial_variables={}, template='\\n{query}\\nDouble check the {dialect} query above for common mistakes, including:\\n- Using NOT IN with NULL values\\n- Using UNION when UNION ALL should have been used\\n- Using BETWEEN for exclusive ranges\\n- Data type mismatch in predicates\\n- Properly quoting identifiers\\n- Using the correct number of arguments for functions\\n- Casting to the correct data type\\n- Using the proper columns for joins\\n\\nIf there are any of the above mistakes, rewrite the query. If there are no mistakes, just reproduce the original query.\\n\\nOutput the final SQL query only.\\n\\nSQL Query: '), llm=ChatTongyi(name='qwen-turbo', client=<class 'dashscope.aigc.generation.Generation'>, model_kwargs={}, dashscope_api_key=SecretStr('**********'), streaming=True), output_parser=StrOutputParser(), llm_kwargs={}))]"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain_community.agent_toolkits import SQLDatabaseToolkit\n",
    "\n",
    "toolkit = SQLDatabaseToolkit(db=db, llm=llm)\n",
    "\n",
    "tools = toolkit.get_tools()\n",
    "\n",
    "tools"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "79d2068d-5fc8-4a33-9979-b9aeefeb5c57",
   "metadata": {},
   "source": [
    "**系统提示词（System Prompt）**\n",
    "\n",
    "我们还希望为智能体加载一个系统提示词。这将包含代理应遵循的行为指令。请注意，下面的提示词包含多个参数，我们将在下方进行赋值。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "348e9e6a-5254-49ab-9602-50f6c7eac26c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 你是一个专门用于与 SQL 数据库交互的代理。\n",
    "# 根据输入的问题，生成一个语法正确的 {dialect} 查询语句并执行，\n",
    "# 然后查看查询结果并返回答案。除非用户明确指定了想要获取的具体示例数量，\n",
    "# 否则你的查询结果最多限制为 {top_k} 条。\n",
    "# 你可以通过相关列对结果进行排序，以返回数据库中最有意义的示例。\n",
    "# 不要从某个特定表中查询所有列，只选择与问题相关的列进行查询。\n",
    "# 在执行查询之前，你必须仔细检查你的查询语句。如果在执行过程中出现错误，\n",
    "# 请重写查询并再次尝试。\n",
    "# 禁止向数据库发送任何数据操作语言（DML）语句（如 INSERT、UPDATE、DELETE、DROP 等）。\n",
    "# 开始时，你始终应该首先查看数据库中的表，了解你可以查询哪些内容。\n",
    "# 不要跳过这一步。\n",
    "# 接着，你应该查询最相关表的结构（schema）。\n",
    "\n",
    "system_message = \"\"\"\n",
    "You are an agent designed to interact with a SQL database.\n",
    "Given an input question, create a syntactically correct {dialect} query to run,\n",
    "then look at the results of the query and return the answer. Unless the user\n",
    "specifies a specific number of examples they wish to obtain, always limit your\n",
    "query to at most {top_k} results.\n",
    "\n",
    "You can order the results by a relevant column to return the most interesting\n",
    "examples in the database. Never query for all the columns from a specific table,\n",
    "only ask for the relevant columns given the question.\n",
    "\n",
    "You MUST double check your query before executing it. If you get an error while\n",
    "executing a query, rewrite the query and try again.\n",
    "\n",
    "DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the\n",
    "database.\n",
    "\n",
    "To start you should ALWAYS look at the tables in the database to see what you\n",
    "can query. Do NOT skip this step.\n",
    "\n",
    "Then you should query the schema of the most relevant tables.\n",
    "\"\"\".format(\n",
    "    dialect=\"SQLite\",\n",
    "    top_k=5,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1311230-001f-435e-bfeb-fafd5a7e459e",
   "metadata": {},
   "source": [
    "**初始化智能体**\n",
    "\n",
    "我们将使用一个预构建的 LangGraph 代理来创建我们的代理。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "27e5eb1c-c45c-4f42-afe6-5e70de3fcd9d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.messages import HumanMessage\n",
    "from langgraph.prebuilt import create_react_agent\n",
    "\n",
    "agent_executor = create_react_agent(llm, tools, prompt=system_message)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "c1f7a970-751f-4164-a221-90ed2f7e363d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "哪个国家的客户花费最多?\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  sql_db_list_tables (call_4df5b22b68d54e9e89753dcall_4df5b22b68d54e9e89753d)\n",
      " Call ID: call_4df5b22b68d54e9e89753dcall_4df5b22b68d54e9e89753d\n",
      "  Args:\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_list_tables\n",
      "\n",
      "Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  sql_db_schema (call_a9c0ee4a7bd14736ac6d76call_a9c0ee4a7bd14736ac6d76call_a9c0ee4a7bd14736ac6d76call_a9c0ee4a7bd14736ac6d76)\n",
      " Call ID: call_a9c0ee4a7bd14736ac6d76call_a9c0ee4a7bd14736ac6d76call_a9c0ee4a7bd14736ac6d76call_a9c0ee4a7bd14736ac6d76\n",
      "  Args:\n",
      "    table_names: Customer\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_schema\n",
      "\n",
      "\n",
      "CREATE TABLE \"Customer\" (\n",
      "\t\"CustomerId\" INTEGER NOT NULL, \n",
      "\t\"FirstName\" NVARCHAR(40) NOT NULL, \n",
      "\t\"LastName\" NVARCHAR(20) NOT NULL, \n",
      "\t\"Company\" NVARCHAR(80), \n",
      "\t\"Address\" NVARCHAR(70), \n",
      "\t\"City\" NVARCHAR(40), \n",
      "\t\"State\" NVARCHAR(40), \n",
      "\t\"Country\" NVARCHAR(40), \n",
      "\t\"PostalCode\" NVARCHAR(10), \n",
      "\t\"Phone\" NVARCHAR(24), \n",
      "\t\"Fax\" NVARCHAR(24), \n",
      "\t\"Email\" NVARCHAR(60) NOT NULL, \n",
      "\t\"SupportRepId\" INTEGER, \n",
      "\tPRIMARY KEY (\"CustomerId\"), \n",
      "\tFOREIGN KEY(\"SupportRepId\") REFERENCES \"Employee\" (\"EmployeeId\")\n",
      ")\n",
      "\n",
      "/*\n",
      "3 rows from Customer table:\n",
      "CustomerId\tFirstName\tLastName\tCompany\tAddress\tCity\tState\tCountry\tPostalCode\tPhone\tFax\tEmail\tSupportRepId\n",
      "1\tLuís\tGonçalves\tEmbraer - Empresa Brasileira de Aeronáutica S.A.\tAv. Brigadeiro Faria Lima, 2170\tSão José dos Campos\tSP\tBrazil\t12227-000\t+55 (12) 3923-5555\t+55 (12) 3923-5566\tluisg@embraer.com.br\t3\n",
      "2\tLeonie\tKöhler\tNone\tTheodor-Heuss-Straße 34\tStuttgart\tNone\tGermany\t70174\t+49 0711 2842222\tNone\tleonekohler@surfeu.de\t5\n",
      "3\tFrançois\tTremblay\tNone\t1498 rue Bélanger\tMontréal\tQC\tCanada\tH2G 1A7\t+1 (514) 721-4711\tNone\tftremblay@gmail.com\t3\n",
      "*/\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  sql_db_schema (call_b516d128054e4be897eb47call_b516d128054e4be897eb47call_b516d128054e4be897eb47call_b516d128054e4be897eb47)\n",
      " Call ID: call_b516d128054e4be897eb47call_b516d128054e4be897eb47call_b516d128054e4be897eb47call_b516d128054e4be897eb47\n",
      "  Args:\n",
      "    table_names: Invoice\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_schema\n",
      "\n",
      "\n",
      "CREATE TABLE \"Invoice\" (\n",
      "\t\"InvoiceId\" INTEGER NOT NULL, \n",
      "\t\"CustomerId\" INTEGER NOT NULL, \n",
      "\t\"InvoiceDate\" DATETIME NOT NULL, \n",
      "\t\"BillingAddress\" NVARCHAR(70), \n",
      "\t\"BillingCity\" NVARCHAR(40), \n",
      "\t\"BillingState\" NVARCHAR(40), \n",
      "\t\"BillingCountry\" NVARCHAR(40), \n",
      "\t\"BillingPostalCode\" NVARCHAR(10), \n",
      "\t\"Total\" NUMERIC(10, 2) NOT NULL, \n",
      "\tPRIMARY KEY (\"InvoiceId\"), \n",
      "\tFOREIGN KEY(\"CustomerId\") REFERENCES \"Customer\" (\"CustomerId\")\n",
      ")\n",
      "\n",
      "/*\n",
      "3 rows from Invoice table:\n",
      "InvoiceId\tCustomerId\tInvoiceDate\tBillingAddress\tBillingCity\tBillingState\tBillingCountry\tBillingPostalCode\tTotal\n",
      "1\t2\t2021-01-01 00:00:00\tTheodor-Heuss-Straße 34\tStuttgart\tNone\tGermany\t70174\t1.98\n",
      "2\t4\t2021-01-02 00:00:00\tUllevålsveien 14\tOslo\tNone\tNorway\t0171\t3.96\n",
      "3\t8\t2021-01-03 00:00:00\tGrétrystraat 63\tBrussels\tNone\tBelgium\t1000\t5.94\n",
      "*/\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  sql_db_query_checker (call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180)\n",
      " Call ID: call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180call_28a65bd751e64cb7a32180\n",
      "  Args:\n",
      "    query: SELECT Customer.Country, SUM(Invoice.Total) AS Total_Spent FROM Customer JOIN Invoice ON Customer.CustomerId = Invoice.CustomerId GROUP BY Customer.Country ORDER BY Total_Spent DESC LIMIT 5\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_query_checker\n",
      "\n",
      "```sql\n",
      "SELECT Customer.Country, SUM(Invoice.Total) AS Total_Spent \n",
      "FROM Customer \n",
      "JOIN Invoice ON Customer.CustomerId = Invoice.CustomerId \n",
      "GROUP BY Customer.Country \n",
      "ORDER BY Total_Spent DESC \n",
      "LIMIT 5\n",
      "```\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  sql_db_query (call_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4d)\n",
      " Call ID: call_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4dcall_e4fca81372ed47d484fc4d\n",
      "  Args:\n",
      "    query: SELECT Customer.Country, SUM(Invoice.Total) AS Total_Spent FROM Customer JOIN Invoice ON Customer.CustomerId = Invoice.CustomerId GROUP BY Customer.Country ORDER BY Total_Spent DESC LIMIT 5\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_query\n",
      "\n",
      "[('USA', 523.06), ('Canada', 303.96), ('France', 195.1), ('Brazil', 190.1), ('Germany', 156.48)]\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "美国的客户花费最多，总花费为523.06。以下是按花费排序的前五个国家：\n",
      "\n",
      "1. 美国, 总花费: 523.06\n",
      "2. 加拿大, 总花费: 303.96\n",
      "3. 法国, 总花费: 195.1\n",
      "4. 巴西, 总花费: 190.1\n",
      "5. 德国, 总花费: 156.48\n"
     ]
    }
   ],
   "source": [
    "question = \"哪个国家的客户花费最多?\"\n",
    "\n",
    "for step in agent_executor.stream(\n",
    "    {\"messages\": [{\"role\": \"user\", \"content\": question}]},\n",
    "    stream_mode=\"values\",\n",
    "):\n",
    "    step[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "132fd4df-a7b0-47ac-99b6-ad91882370df",
   "metadata": {},
   "source": [
    "请注意，该代理会执行多个查询，直到获得所需的信息为止：\n",
    "\n",
    "- 列出可用的表；\n",
    "- 获取三个表的结构（schema）；\n",
    "- 通过连接操作（join）查询多个表。\n",
    "\n",
    "代理随后可以利用最终查询的结果来生成对原始问题的回答。\n",
    "\n",
    "该代理同样可以处理定性类问题："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "807f68b7-0f99-426d-9960-7a4273caa98d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "描述播放列表曲目\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  sql_db_list_tables (call_6c7963dc6d5f415db0485bcall_6c7963dc6d5f415db0485b)\n",
      " Call ID: call_6c7963dc6d5f415db0485bcall_6c7963dc6d5f415db0485b\n",
      "  Args:\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_list_tables\n",
      "\n",
      "Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "在数据库中存在多个表，其中可能与播放列表曲目相关的表有Playlist, Track和PlaylistTrack。为了查询播放列表中的曲目信息，我们需要查看这些表的结构。\n",
      "\n",
      "接下来，我将查询这些表的结构。\n",
      "Tool Calls:\n",
      "  sql_db_schema (call_63f1049c54cf4fd7956825call_63f1049c54cf4fd7956825call_63f1049c54cf4fd7956825call_63f1049c54cf4fd7956825call_63f1049c54cf4fd7956825)\n",
      " Call ID: call_63f1049c54cf4fd7956825call_63f1049c54cf4fd7956825call_63f1049c54cf4fd7956825call_63f1049c54cf4fd7956825call_63f1049c54cf4fd7956825\n",
      "  Args:\n",
      "    table_names: Playlist,Track,PlaylistTrack\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_schema\n",
      "\n",
      "\n",
      "CREATE TABLE \"Playlist\" (\n",
      "\t\"PlaylistId\" INTEGER NOT NULL, \n",
      "\t\"Name\" NVARCHAR(120), \n",
      "\tPRIMARY KEY (\"PlaylistId\")\n",
      ")\n",
      "\n",
      "/*\n",
      "3 rows from Playlist table:\n",
      "PlaylistId\tName\n",
      "1\tMusic\n",
      "2\tMovies\n",
      "3\tTV Shows\n",
      "*/\n",
      "\n",
      "\n",
      "CREATE TABLE \"PlaylistTrack\" (\n",
      "\t\"PlaylistId\" INTEGER NOT NULL, \n",
      "\t\"TrackId\" INTEGER NOT NULL, \n",
      "\tPRIMARY KEY (\"PlaylistId\", \"TrackId\"), \n",
      "\tFOREIGN KEY(\"TrackId\") REFERENCES \"Track\" (\"TrackId\"), \n",
      "\tFOREIGN KEY(\"PlaylistId\") REFERENCES \"Playlist\" (\"PlaylistId\")\n",
      ")\n",
      "\n",
      "/*\n",
      "3 rows from PlaylistTrack table:\n",
      "PlaylistId\tTrackId\n",
      "1\t3402\n",
      "1\t3389\n",
      "1\t3390\n",
      "*/\n",
      "\n",
      "\n",
      "CREATE TABLE \"Track\" (\n",
      "\t\"TrackId\" INTEGER NOT NULL, \n",
      "\t\"Name\" NVARCHAR(200) NOT NULL, \n",
      "\t\"AlbumId\" INTEGER, \n",
      "\t\"MediaTypeId\" INTEGER NOT NULL, \n",
      "\t\"GenreId\" INTEGER, \n",
      "\t\"Composer\" NVARCHAR(220), \n",
      "\t\"Milliseconds\" INTEGER NOT NULL, \n",
      "\t\"Bytes\" INTEGER, \n",
      "\t\"UnitPrice\" NUMERIC(10, 2) NOT NULL, \n",
      "\tPRIMARY KEY (\"TrackId\"), \n",
      "\tFOREIGN KEY(\"MediaTypeId\") REFERENCES \"MediaType\" (\"MediaTypeId\"), \n",
      "\tFOREIGN KEY(\"GenreId\") REFERENCES \"Genre\" (\"GenreId\"), \n",
      "\tFOREIGN KEY(\"AlbumId\") REFERENCES \"Album\" (\"AlbumId\")\n",
      ")\n",
      "\n",
      "/*\n",
      "3 rows from Track table:\n",
      "TrackId\tName\tAlbumId\tMediaTypeId\tGenreId\tComposer\tMilliseconds\tBytes\tUnitPrice\n",
      "1\tFor Those About To Rock (We Salute You)\t1\t1\t1\tAngus Young, Malcolm Young, Brian Johnson\t343719\t11170334\t0.99\n",
      "2\tBalls to the Wall\t2\t2\t1\tU. Dirkschneider, W. Hoffmann, H. Frank, P. Baltes, S. Kaufmann, G. Hoffmann\t342562\t5510424\t0.99\n",
      "3\tFast As a Shark\t3\t2\t1\tF. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman\t230619\t3990994\t0.99\n",
      "*/\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "从Playlist, Track和PlaylistTrack表的结构来看，我们可以看到Playlist表包含了播放列表的信息，Track表包含了曲目的详细信息，而PlaylistTrack表则将这两个表联系起来，它记录了哪些曲目属于哪些播放列表。\n",
      "\n",
      "例如，我们可以查询音乐播放列表（PlaylistId为1）中的曲目信息。我们将使用PlaylistTrack表来获取曲目ID，然后使用Track表来获取曲目详细信息。\n",
      "\n",
      "让我们先检查一下查询是否正确。\n",
      "Tool Calls:\n",
      "  sql_db_query_checker (call_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3d)\n",
      " Call ID: call_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3dcall_eea63db2e57741baaebe3d\n",
      "  Args:\n",
      "    query: SELECT t.TrackId, t.Name, t.AlbumId, t.MediaTypeId, t.GenreId, t.Composer, t.Milliseconds, t.Bytes, t.UnitPrice FROM PlaylistTrack pt INNER JOIN Track t ON pt.TrackId = t.TrackId WHERE pt.PlaylistId = 1 LIMIT 5\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_query_checker\n",
      "\n",
      "```sql\n",
      "SELECT t.TrackId, t.Name, t.AlbumId, t.MediaTypeId, t.GenreId, t.Composer, t.Milliseconds, t.Bytes, t.UnitPrice \n",
      "FROM PlaylistTrack pt \n",
      "INNER JOIN Track t ON pt.TrackId = t.TrackId \n",
      "WHERE pt.PlaylistId = 1 \n",
      "LIMIT 5;\n",
      "```\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "查询看起来是正确的。现在，我将执行这个查询并查看结果。\n",
      "Tool Calls:\n",
      "  sql_db_query (call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15)\n",
      " Call ID: call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15call_6d4a09a2e7494d8ab24a15\n",
      "  Args:\n",
      "    query: SELECT t.TrackId, t.Name, t.AlbumId, t.MediaTypeId, t.GenreId, t.Composer, t.Milliseconds, t.Bytes, t.UnitPrice FROM PlaylistTrack pt INNER JOIN Track t ON pt.TrackId = t.TrackId WHERE pt.PlaylistId = 1 LIMIT 5\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_query\n",
      "\n",
      "[(1, 'For Those About To Rock (We Salute You)', 1, 1, 1, 'Angus Young, Malcolm Young, Brian Johnson', 343719, 11170334, 0.99), (2, 'Balls to the Wall', 2, 2, 1, 'U. Dirkschneider, W. Hoffmann, H. Frank, P. Baltes, S. Kaufmann, G. Hoffmann', 342562, 5510424, 0.99), (3, 'Fast As a Shark', 3, 2, 1, 'F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman', 230619, 3990994, 0.99), (4, 'Restless and Wild', 3, 2, 1, 'F. Baltes, R.A. Smith-Diesel, S. Kaufman, U. Dirkscneider & W. Hoffman', 252051, 4331779, 0.99), (5, 'Princess of the Dawn', 3, 2, 1, 'Deaffy & R.A. Smith-Diesel', 375418, 6290521, 0.99)]\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "播放列表ID为1的音乐播放列表包含以下曲目：\n",
      "\n",
      "1. 曲目ID: 1, 曲目名: 'For Those About To Rock (We Salute You)', 所属专辑ID: 1, 媒体类型ID: 1, 风格ID: 1, 创作者: 'Angus Young, Malcolm Young, Brian Johnson', 毫秒时长: 343719, 字节数: 11170334, 单价: 0.99\n",
      "2. 曲目ID: 2, 曲目名: 'Balls to the Wall', 所属专辑ID: 2, 媒体类型ID: 2, 风格ID: 1, 创作者: 'U. Dirkschneider, W. Hoffmann, H. Frank, P. Baltes, S. Kaufmann, G. Hoffmann', 毫秒时长: 342562, 字节数: 5510424, 单价: 0.99\n",
      "3. 曲目ID: 3, 曲目名: 'Fast As a Shark', 所属专辑ID: 3, 媒体类型ID: 2, 风格ID: 1, 创作者: 'F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman', 毫秒时长: 230619, 字节数: 3990994, 单价: 0.99\n",
      "4. 曲目ID: 4, 曲目名: 'Restless and Wild', 所属专辑ID: 3, 媒体类型ID: 2, 风格ID: 1, 创作者: 'F. Baltes, R.A. Smith-Diesel, S. Kaufman, U. Dirkscneider & W. Hoffman', 毫秒时长: 252051, 字节数: 4331779, 单价: 0.99\n",
      "5. 曲目ID: 5, 曲目名: 'Princess of the Dawn', 所属专辑ID: 3, 媒体类型ID: 2, 风格ID: 1, 创作者: 'Deaffy & R.A. Smith-Diesel', 毫秒时长: 375418, 字节数: 6290521, 单价: 0.99\n",
      "\n",
      "这些是音乐播放列表中的前五首曲目。\n"
     ]
    }
   ],
   "source": [
    "question = \"描述播放列表曲目\"\n",
    "\n",
    "for step in agent_executor.stream(\n",
    "    {\"messages\": [{\"role\": \"user\", \"content\": question}]},\n",
    "    stream_mode=\"values\",\n",
    "):\n",
    "    step[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1d2bc3af-aaeb-4416-851f-70704337fd76",
   "metadata": {},
   "source": [
    "**处理高基数列（High-cardinality Columns）**\n",
    "\n",
    "为了对包含专有名词（如地址、歌曲名或艺术家名）的列进行筛选，我们首先需要检查拼写是否正确，以便准确地过滤数据。\n",
    "\n",
    "我们可以通过创建一个包含数据库中所有不同专有名词的向量存储（vector store）来实现这一点。然后，每当用户在问题中使用了一个专有名词时，我们可以让代理查询这个向量存储，以找到该词的正确拼写。通过这种方式，代理可以在构建目标查询之前，确保自己理解用户所指的是哪个实体。\n",
    "\n",
    "首先，我们需要获取每个想要的实体的唯一值，为此我们定义一个函数，将查询结果解析为元素列表："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "e961b690-b920-4491-ae9a-80ab653ef43d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['A TempestadeTempestade Ou O Livro Dos Dias',\n",
       " 'The Number of The Beast',\n",
       " 'Bach: Toccata & Fugue in D Minor',\n",
       " 'Vozes do MPB',\n",
       " 'Bach: Orchestral Suites Nos.  -']"
      ]
     },
     "execution_count": 24,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import ast\n",
    "import re\n",
    "\n",
    "\n",
    "def query_as_list(db, query):\n",
    "    res = db.run(query)\n",
    "    res = [el for sub in ast.literal_eval(res) for el in sub if el]\n",
    "    res = [re.sub(r\"\\b\\d+\\b\", \"\", string).strip() for string in res]\n",
    "    return list(set(res))\n",
    "\n",
    "\n",
    "artists = query_as_list(db, \"SELECT Name FROM Artist\")\n",
    "albums = query_as_list(db, \"SELECT Title FROM Album\")\n",
    "albums[:5]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0c5cc55a-0bcd-4d37-a853-824c6a9a4d98",
   "metadata": {},
   "source": [
    "#### 逐行解析：\n",
    "\n",
    "- `res = db.run(query)`  \n",
    "  执行传入的 SQL 查询语句，并将结果保存在 `res` 中。  \n",
    "  假设返回的是字符串形式的结果，比如类似 `[('Artist1',), ('Artist2',)]` 的结构。\n",
    "\n",
    "- `res = [el for sub in ast.literal_eval(res) for el in sub if el]`  \n",
    "  使用 `ast.literal_eval()` 将字符串形式的结果转换为 Python 列表/元组结构，  \n",
    "  然后通过列表推导式提取每个子项中的元素（例如 `'Artist1'`），并过滤掉空值。\n",
    "\n",
    "- `res = [re.sub(r\"\\b\\d+\\b\", \"\", string).strip() for string in res]`  \n",
    "  使用正则表达式去掉字符串中的独立数字（`\\b\\d+\\b` 表示被单词边界包围的数字），  \n",
    "  然后使用 `.strip()` 去除首尾的空白字符。\n",
    "\n",
    "- `return list(set(res))`  \n",
    "  返回一个去重后的列表（通过 `set` 去重，再转回 `list`）。\n",
    "\n",
    "---\n",
    "\n",
    "### **3. 查询数据库中的艺术家和专辑名称**\n",
    "\n",
    "```python\n",
    "artists = query_as_list(db, \"SELECT Name FROM Artist\")\n",
    "albums = query_as_list(db, \"SELECT Title FROM Album\")\n",
    "```\n",
    "\n",
    "- 这两行分别从数据库中查询出所有艺术家的名字和专辑的标题，并调用 `query_as_list` 函数对结果进行清洗和去重。\n",
    "\n",
    "---\n",
    "\n",
    "### **4. 显示前5个专辑标题**\n",
    "\n",
    "```python\n",
    "albums[:5]\n",
    "```\n",
    "\n",
    "- 显示 `albums` 列表中的前5个元素，通常用于快速查看数据内容。\n",
    "\n",
    "---\n",
    "\n",
    "### ✅ 总结\n",
    "\n",
    "这段代码的主要作用是：\n",
    "\n",
    "1. 从数据库中查询出艺术家名和专辑标题；\n",
    "2. 清洗这些数据（如去除数字、空值）；\n",
    "3. 去重后以列表形式返回；\n",
    "4. 最后展示部分结果以便检查。\n",
    "\n",
    "这在构建向量存储或实体识别系统时非常有用，尤其是在处理高基数列（如歌曲名、艺术家名等）时。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "bdf5d625-db0b-49f2-aac8-284d4a53539c",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.embeddings.dashscope import DashScopeEmbeddings\n",
    "# 初始化 Qwen Embedding 模型\n",
    "embeddings = DashScopeEmbeddings(model=\"text-embedding-v1\")  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "f7d4c89c-62ef-419c-83ca-ab313056834d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.vectorstores import InMemoryVectorStore\n",
    "\n",
    "vector_store = InMemoryVectorStore(embeddings)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ed26b55e-9592-483a-b1d1-14b39b68a522",
   "metadata": {},
   "source": [
    "我们现在可以构建一个检索工具，可以搜索数据库中的相关专有名词："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "22af1876-00a6-4e15-b9af-4515853a0365",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.agents.agent_toolkits import create_retriever_tool\n",
    "\n",
    "_ = vector_store.add_texts(artists + albums)\n",
    "retriever = vector_store.as_retriever(search_kwargs={\"k\": 5})\n",
    "# 用于查找需要过滤的值。输入是对专有名词的近似拼写，输出是有效的专有名词。请使用与搜索词最相似的名词。\n",
    "description = (\n",
    "    \"Use to look up values to filter on. Input is an approximate spelling \"\n",
    "    \"of the proper noun, output is valid proper nouns. Use the noun most \"\n",
    "    \"similar to the search.\"\n",
    ")\n",
    "retriever_tool = create_retriever_tool(\n",
    "    retriever,\n",
    "    name=\"search_proper_nouns\",\n",
    "    description=description,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "e1f08cc6-f0a9-42d7-b73d-90ffebf69cdb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Alice In Chains\n",
      "\n",
      "Adrian Leaper & Doreen de Feis\n",
      "\n",
      "Chill: Brazil (Disc )\n",
      "\n",
      "Elis Regina-Minha História\n",
      "\n",
      "Christina Aguilera featuring BigElf\n"
     ]
    }
   ],
   "source": [
    "print(retriever_tool.invoke(\"Alice Chains\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fdc01b9f-40e7-47be-98cc-c30378015104",
   "metadata": {},
   "source": [
    "这样，如果代理确定需要按照“Alice Chains”的思路编写基于艺术家的过滤器，它可以首先使用检索器工具来观察列的相关值。\n",
    "\n",
    "将其放在一起："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "91286c24-d420-474d-b77e-7c8e64daea30",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 如果需要根据专有名词（如名称）进行过滤，你必须始终首先使用 `search_proper_nouns` 工具查找过滤值！\n",
    "# 不要尝试猜测正确的名称——请使用这个工具来查找相似的名称。”\n",
    "suffix = (\n",
    "    \"If you need to filter on a proper noun like a Name, you must ALWAYS first look up \"\n",
    "    \"the filter value using the 'search_proper_nouns' tool! Do not try to \"\n",
    "    \"guess at the proper name - use this function to find similar ones.\"\n",
    ")\n",
    "\n",
    "system = f\"{system_message}\\n\\n{suffix}\"\n",
    "\n",
    "tools.append(retriever_tool)\n",
    "\n",
    "agent = create_react_agent(llm, tools, prompt=system)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "0ddc9a5f-c2ff-45c3-83c1-e87619d67ca8",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "Alis in chain 有多少张专辑?\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  sql_db_list_tables (call_f54cd9445610451cad794ccall_f54cd9445610451cad794c)\n",
      " Call ID: call_f54cd9445610451cad794ccall_f54cd9445610451cad794c\n",
      "  Args:\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_list_tables\n",
      "\n",
      "Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  sql_db_schema (call_11e127a668724e28a4d7bccall_11e127a668724e28a4d7bccall_11e127a668724e28a4d7bccall_11e127a668724e28a4d7bc)\n",
      " Call ID: call_11e127a668724e28a4d7bccall_11e127a668724e28a4d7bccall_11e127a668724e28a4d7bccall_11e127a668724e28a4d7bc\n",
      "  Args:\n",
      "    table_names: Album\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_schema\n",
      "\n",
      "\n",
      "CREATE TABLE \"Album\" (\n",
      "\t\"AlbumId\" INTEGER NOT NULL, \n",
      "\t\"Title\" NVARCHAR(160) NOT NULL, \n",
      "\t\"ArtistId\" INTEGER NOT NULL, \n",
      "\tPRIMARY KEY (\"AlbumId\"), \n",
      "\tFOREIGN KEY(\"ArtistId\") REFERENCES \"Artist\" (\"ArtistId\")\n",
      ")\n",
      "\n",
      "/*\n",
      "3 rows from Album table:\n",
      "AlbumId\tTitle\tArtistId\n",
      "1\tFor Those About To Rock We Salute You\t1\n",
      "2\tBalls to the Wall\t2\n",
      "3\tRestless and Wild\t2\n",
      "*/\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  sql_db_schema (call_a26bf6366e2d4389bcfa5fcall_a26bf6366e2d4389bcfa5fcall_a26bf6366e2d4389bcfa5fcall_a26bf6366e2d4389bcfa5f)\n",
      " Call ID: call_a26bf6366e2d4389bcfa5fcall_a26bf6366e2d4389bcfa5fcall_a26bf6366e2d4389bcfa5fcall_a26bf6366e2d4389bcfa5f\n",
      "  Args:\n",
      "    table_names: Artist\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_schema\n",
      "\n",
      "\n",
      "CREATE TABLE \"Artist\" (\n",
      "\t\"ArtistId\" INTEGER NOT NULL, \n",
      "\t\"Name\" NVARCHAR(120), \n",
      "\tPRIMARY KEY (\"ArtistId\")\n",
      ")\n",
      "\n",
      "/*\n",
      "3 rows from Artist table:\n",
      "ArtistId\tName\n",
      "1\tAC/DC\n",
      "2\tAccept\n",
      "3\tAerosmith\n",
      "*/\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  sql_db_query_checker (call_49050912013046d281866bcall_49050912013046d281866bcall_49050912013046d281866bcall_49050912013046d281866bcall_49050912013046d281866bcall_49050912013046d281866bcall_49050912013046d281866bcall_49050912013046d281866bcall_49050912013046d281866b)\n",
      " Call ID: call_49050912013046d281866bcall_49050912013046d281866bcall_49050912013046d281866bcall_49050912013046d281866bcall_49050912013046d281866bcall_49050912013046d281866bcall_49050912013046d281866bcall_49050912013046d281866bcall_49050912013046d281866b\n",
      "  Args:\n",
      "    query: SELECT COUNT(*) FROM Album WHERE ArtistId = (SELECT ArtistId FROM Artist WHERE Name = 'Alis in chain')\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_query_checker\n",
      "\n",
      "```sql\n",
      "SELECT COUNT(*) \n",
      "FROM Album \n",
      "WHERE ArtistId = (SELECT ArtistId \n",
      "                  FROM Artist \n",
      "                  WHERE Name = 'Alis in chain');\n",
      "```\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  search_proper_nouns (call_871f73919f0c4b37b04796call_871f73919f0c4b37b04796call_871f73919f0c4b37b04796call_871f73919f0c4b37b04796)\n",
      " Call ID: call_871f73919f0c4b37b04796call_871f73919f0c4b37b04796call_871f73919f0c4b37b04796call_871f73919f0c4b37b04796\n",
      "  Args:\n",
      "    query: Alis in chain\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: search_proper_nouns\n",
      "\n",
      "Alice In Chains\n",
      "\n",
      "Elis Regina\n",
      "\n",
      "Alanis Morissette\n",
      "\n",
      "Surfing with the Alien (Remastered)\n",
      "\n",
      "Cássia Eller - Coleção Sem Limite [Disc ]\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  sql_db_query (call_6e99daab2af14a099d545dcall_6e99daab2af14a099d545dcall_6e99daab2af14a099d545dcall_6e99daab2af14a099d545dcall_6e99daab2af14a099d545dcall_6e99daab2af14a099d545dcall_6e99daab2af14a099d545dcall_6e99daab2af14a099d545dcall_6e99daab2af14a099d545d)\n",
      " Call ID: call_6e99daab2af14a099d545dcall_6e99daab2af14a099d545dcall_6e99daab2af14a099d545dcall_6e99daab2af14a099d545dcall_6e99daab2af14a099d545dcall_6e99daab2af14a099d545dcall_6e99daab2af14a099d545dcall_6e99daab2af14a099d545dcall_6e99daab2af14a099d545d\n",
      "  Args:\n",
      "    query: SELECT COUNT(*) FROM Album WHERE ArtistId = (SELECT ArtistId FROM Artist WHERE Name = 'Alice In Chains')\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: sql_db_query\n",
      "\n",
      "[(1,)]\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "Alice In Chains 有1张专辑。\n"
     ]
    }
   ],
   "source": [
    "question = \"Alis in chain 有多少张专辑?\"\n",
    "\n",
    "for step in agent.stream(\n",
    "    {\"messages\": [{\"role\": \"user\", \"content\": question}]},\n",
    "    stream_mode=\"values\",\n",
    "):\n",
    "    step[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e8826cdc-aef5-43e5-be42-95df272aa15a",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
